# python-resmon - Python tools for Resmon monitoring and the Circonus API
# Copyright (c) 2010 Crown coypright
# 
# This file is part of python-resmon.
# 
# python-resmon is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# python-resmon is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with python-resmon.  If not, see <http://www.gnu.org/licenses/>.

import re
import time
import datetime
import urllib
import urllib2
import urlparse
import json
import os.path

BASE_URL = 'https://circonus.com/api/'

import warnings



class APIError(Exception):
    """An error returned from the Circonus API"""
    def message(self):
        return self.args[0]['error']


class JsNode(object):
    def __init__(self, api, json):
        self.api = api
        self.json = json

    def __getattr__(self, key):
        try:
            return self.json[key]
        except KeyError:
            raise AttributeError(key)

    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__, self.name)


class Bundle(object):
    """A bundle is a collection of checks of a resource by different agents."""
    def __init__(self, api, id, check):
        self.id = id
        self.checks = [check]

    def add_check(self):
        self.checks.append(check)


class Check(JsNode):
    """A checks run on a specific agent"""

    def list_metrics(self):
        return [Metric(self.api, js) for js in self.api._request('json/list_metrics', {'check_id': self.check_id})]

    def search_metrics(self, str):
        return [m for m in self.list_metrics() if m.name.startswith(str)]

    def update(self, **kwargs):
        params = {
            'bundle_id': self.bundle_id
        }
        params.update(kwargs)
        resp = self.api._post('json/edit_check_bundle', post_params=params)
        check_id = resp['successes'][0]['check_id']
        newchk = self.api.get_check_by_id(check_id)
        self.json = newchk.json

    def set_enabled(self, enabled=False):
        if enabled:
            url = 'json/enable_check'
        else:
            url = 'json/disable_check'

        return self.api._post(url, post_params={'check_id': self.check_id})


class Agent(JsNode):
    """An agent which performs checks"""


class Metric(JsNode):
    """A metric contained within a Check"""

    def list_rules(self):
        try:
            return self.__rules[:]
        except AttributeError:
            result = [Rule(self, self.api, js) for js in self.api._request('json/list_rules', {'check_id': self.check_id, 'metric_name': self.name})]
            result.sort(key=lambda r: r.order)
            self.__rules = result
            return result[:]

    def _get_order(self, r):
        for i, rule in enumerate(self.__rules):
            if rule is r:
                return i + 1
        raise KeyError("Not found")

    def add_contact_group(self, contact_group, severity=1):
        """Add contact_group as a group to contact at when rules trigger alert level severity"""
        resp = self.api._post('json/add_rule_contact_group', post_params={'check_id': self.check_id, 'metric_name': self.name, 'contact_group_id': contact_group.id, 'severity': severity})
        return resp

    def remove_contact_group(self, contact_group, severity=1):
        """Add contact_group as a group to contact at when rules trigger alert level severity"""
        resp = self.api._post('json/remove_rule_contact_group', post_params={'check_id': self.check_id, 'metric_name': self.name, 'contact_group_id': contact_group.id, 'severity': severity})
        return resp

    def clear_rules(self, r):
        for i in len(self.list_rules()):
            self.api._post('json/remove_metric_rule', post_params={'check_id': self.check_id, 'metric_name': self.name, 'order': 1})
        self.__rules = []

    def delete(self, r):
        i = r.get_order()
        resp = self.api._post('json/remove_metric_rule', post_params={'check_id': self.check_id, 'metric_name': self.name, 'order': i})
        self.__rules.pop(i - 1)
        return resp

    def append_rule(self, criteria, value, severity=1):
        return self.create_rule(**{'criteria': criteria, 'value': value, 'severity': severity, 'order': 1 << 31})

    def append_on_absence(self, severity=1):
        return self.create_rule(**{'criteria': 'on absence', 'value': 1, 'severity': severity, 'order': 1 << 31})

    def create_rule(self, **kwargs):
        post = {
            'check_id': self.check_id,
            'metric_name': self.name,
        }
        post.update(kwargs)
        try:
            del(self.__rules)
        except AttributeError:
            pass
        return self.api._post('json/add_metric_rule', post_params=post)


class Rule(JsNode):
    """A rule that is used to check a metric"""
    def __init__(self, metric, api, json):
        super(Rule, self).__init__(api, json)
        self.metric = metric

    def get_order(self):
        return self.metric._get_order(self)
    order = property(get_order)

    def _name(self):
        return "%s %s %s" % (self.metric_name, self.criteria, self.value)
    name = property(_name)

    def delete(self):
        self.metric.delete(self)


class ContactGroup(JsNode):
    """A group of people to contact when a rule is matched."""

    def _id(self):
        return self.json['contact_group_id']
    id = property(_id)


class Alert(JsNode):
    """A notification of an alert or clearance of an alert"""
    def _id(self):
        return self.json['alert_id']
    id = property(_id)

    def _name(self):
        return "%s: %s %s" % (self.check_name, self.metric_name, self.value)
    name = property(_name)

    def has_cleared(self):
        return 'cleared' in self.json

    def occurred_date(self):
        "Thu, 23 Sep 2010 17:53:17"
        datetime.strptime('')



class MultipleObjectsReturned(Exception):
    """A call which the user has intended to return a single object found mode than one match object."""


class CirconusAPI(object):
    def __init__(self, email, password, account, debug=False):
        self.email = email
        self.password = password
        self.account = account
        self.debug = debug

        try:
            from keepalive import HTTPHandler
        except ImportError:
            from warnings import warn
            warn('The circonus module runs faster using keepalive.py, available as part of URLGrabber. See http://linux.duke.edu/urlgrabber/')
            self.opener = urllib2.build_opener()
        else:
            keepalive_handler = HTTPHandler()
            self.opener = urllib2.build_opener(keepalive_handler)

    def get_default_args(self):
        return {
            'email': self.email,
            'password': self.password,
            'account': self.account,
        }

    def _request(self, url, params={}):
        args = self.get_default_args() 
        args.update(params)
        data = urllib.urlencode(args)

        url = urlparse.urljoin(BASE_URL, url) + '?' + data
        if self.debug:
            print 'GET', url
        resp = self.opener.open(url)
        responsejson = json.loads(resp.read())
#        if self.debug:
#            print "GET response", responsejson
        return responsejson

    def _encode(self, args):
        enc = []
        for k, v in args:
            if isinstance(v, float):
                enc.append((k, '%f' % v))
            else:
                enc.append((k, v))
        return urllib.urlencode(enc)

    def _post(self, url, post_params={}):
        ps = list(self.get_default_args().items())
        for k, v in post_params.items():
            if isinstance(v, list) or isinstance(v, tuple):
                for val in v:
                    ps.append((k, val))
            else:
                ps.append((k, v))
        postdata = self._encode(ps)

        url = urlparse.urljoin(BASE_URL, url)
        if self.debug:
            print 'POST', url, postdata
        try:
            resp = urllib2.urlopen(url, postdata)
        except urllib2.HTTPError, e:
            responsejson = json.loads(e.read())
        else:
            responsejson = json.loads(resp.read())
#        if self.debug:
#            print "POST response", responsejson
        if not responsejson['success']:
            raise APIError(responsejson)
        return responsejson

    def get_check_by_id(self, check_id):
        candidates = [c for c in self.list_checks() if c.check_id == int(check_id)]
        if len(candidates) == 1:
            return candidates[0]
        elif not candidates:
            raise KeyError("No such check_id %s" % check_id) 
        else:
            raise MultipleObjectsReturned("%d objects found for check_id %d (expected 1)." % (len(candidates), check_id))

    def list_checks(self):
        return [Check(self, js) for js in self._request('json/list_checks', params={'active': 'true'})]

    def list_agents(self):
        return [Agent(self, js) for js in self._request('json/list_agents')]

    def list_alerts(self, start, end=None):
        """Lists alerts between start and end (unix times). If end is omitted, defaults to now."""
        if end is None:
            end = int(time.time())
        return [Alert(self, js) for js in self._request('json/list_alerts', params={'start_epoch': start, 'end_epoch': end})]

    def list_contact_groups(self):
        return [ContactGroup(self, js) for js in self._request('json/list_contact_groups')]

    def find_contact_group_by_name(self, name):
        candidates = [c for c in self.list_contact_groups() if c.name == name]
        if len(candidates) == 1:
            return candidates[0]
        elif not candidates:
            raise KeyError("No such contact group %s" % name) 
        else:
            raise MultipleObjectsReturned("%d objects found for contact group \"%s\" (expected 1)." % (len(candidates), name))

    def create_bundle(self, **kwargs):
        resp = self._post('json/add_check_bundle', post_params=kwargs)
        check_id = resp['successes'][0]['check_id']
        return self.get_check_by_id(check_id)

    def get_checks_by_name_match(self, match):
        checks = []
        for bundle in self.list_checks():
            if match in bundle.name:
                checks.append(bundle)
        return checks

    @staticmethod
    def from_config(debug=False):
        """Search a list of paths for configuration files containing Circonus
        credentials and construct an API instance.
        
        The paths searched are:

         * circonus.cfg in the current directory
         * ~/.circonus.cfg
         * /etc/circonus.cfg
        
        """
        from ConfigParser import SafeConfigParser
        cfg = SafeConfigParser()
        cfg.read(['circonus.cfg', os.path.expanduser('~/.circonus.cfg'), '/etc/circonus.cfg'])
        account = cfg.get('auth', 'account')
        email = cfg.get('auth', 'email')
        password = cfg.get('auth', 'password')
        return CirconusAPI(email, password, account, debug=debug)


if __name__ == '__main__':
    c = CirconusAPI.from_config()
    print c.list_checks()[0].json
